/*
 * SmartEventBus.java
 *
 * Created on 26 February 2005, 13:59
 *
 * Copyright 2008 the Desert Labs Project (http://desertlabs.us)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package us.desertlabs.event;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import us.desertlabs.ClassHierarchyListFactory;

/**
 * <P>
 * Instances of this class provide access to the auto-routing event system built
 * using <CODE>SmartEvent</CODE> subclasses. To use it, just get an instance
 * by calling one of the <CODE>getBus</CODE> methods, then add the object that
 * should hear events using the <CODE>addListener</CODE> method. The object's
 * class will be examined, and based on the parameters of its <CODE>hear</CODE>
 * methods, it will begin receiving the appropriate types of events.
 * </P>
 * <P>
 * Events are distributed to all listeners that listen to any of the
 * superclasses of the event. For example, if there is an event type "A" and an
 * event type "B" that extends type "A", listeners to type "A" will receive all
 * events of <I>both</I> types. Listeners to type "B", however, will only
 * receive events of that type (and any future subclasses of it). Hence, to
 * listen to every event fired, one can register to listen to "SmartEvent"
 * itself.
 * </P>
 *
 * @author Brandon Franklin
 */
public class SmartEventBus {

    /** The <CODE>SmartEventBus</CODE> shared instances */
    static final private Map<Object, SmartEventBus> INSTANCES = Collections.synchronizedMap( new WeakHashMap<Object, SmartEventBus>() );

    /**
     * Returns the default instance of the <CODE>SmartEventBus</CODE>,
     * creating it if necessary. This is exactly the same as calling <CODE>getBus(
     * null );</CODE>.
     *
     * @return the default instance of the <CODE>SmartEventBus</CODE>
     */
    static public SmartEventBus getBus() {
        return getBus( null );
    }

    /**
     * Returns the instance of <CODE>SmartEventBus</CODE> that is associated
     * with the supplied key object, creating it if necessary.
     *
     * @param key the <CODE>Object</CODE> that identifies the <CODE>SmartEventBus</CODE>
     *        instance to be returned
     * @return the instance of <CODE>SmartEventBus</CODE> that is associated
     *         with the supplied key object
     */
    static public SmartEventBus getBus( final Object key ) {
        SmartEventBus instance = INSTANCES.get( key );
        if( instance == null ) {
            instance = new SmartEventBus();
            INSTANCES.put( key, instance );
        }
        return instance;
    }

    /** The <CODE>Map</CODE> of event types to that type's listeners */
    final private Map<Class<?>, Set<WeakReference<Object>>> listeners = new HashMap<Class<?>, Set<WeakReference<Object>>>();

    /**
     * Creates a new instance of <CODE>SmartEventBus</CODE>. Made private to
     * prevent direct use.
     */
    private SmartEventBus() {
    }

    /**
     * Adds the supplied <CODE>Object</CODE> as an event listener. The events
     * that will be delivered to the listener are determined by introspection of
     * the <CODE>Object</CODE>'s datatype. Any method called "hear" will
     * indicate a notification method, and the datatpe of the method will
     * indicate the type of event to listen for, provided the datatype is <CODE>SmartEvent</CODE>
     * or a subclass of it.
     *
     * @param listener the <CODE>Object</CODE> to add as an event listener
     */
    public void addListener( final Object listener ) {

        // Pull out all the "hear" methods
        final Set<Method> hearMethods = new HashSet<Method>();
        for( final Method method : listener.getClass().getMethods() ) {
            if( method.getName().equals( "hear" ) ) {
                hearMethods.add( method );
            }
        }

        // Examine every "hear" method
        for( final Method method : hearMethods ) {

            final Class<?>[] paramTypes = method.getParameterTypes();

            // Disqualify malformed candidates
            if( (paramTypes.length == 1) && (SmartEvent.class.isAssignableFrom( paramTypes[0] )) ) {
                addTypeSpecificListener( listener, paramTypes[0] );
            }
        }

    }

    /**
     * Fires the supplied <CODE>SmartEvent</CODE>, making it heard by
     * listeners to its type or any of its supertypes.
     *
     * @param event the <CODE>SmartEvent</CODE> to be distributed
     */
    public void fire( final SmartEvent<?> event ) {

        for( final Class<?> type : ClassHierarchyListFactory.createSuperclassList( event.getClass() ) ) {

            if( type.equals( Object.class ) ) {
                continue;
            }

            final Set<WeakReference<Object>> typeListeners = listeners.get( type );
            if( typeListeners != null ) {

                final Collection<WeakReference<Object>> deadRefs = new LinkedList<WeakReference<Object>>();
                for( final WeakReference<Object> listenerRef : typeListeners ) {
                    final Object listener = listenerRef.get();
                    if( listener != null ) {

                        Method method = null;
                        try {
                            method = listener.getClass().getMethod( "hear", type );

                        } catch( final NoSuchMethodException nsme ) {
                            // This should theoretically never be true,
                            // but try to handle it gracefully anyway.
                            deadRefs.add( listenerRef );
                            continue;
                        }

                        Object result = null;
                        try {
                            result = method.invoke( listener, event );
                        } catch( final IllegalAccessException iae ) {
                            Logger.getLogger( getClass().getName() ).log( Level.WARNING, "Exception while trying to fire event to listener: " + listener, iae );
                        } catch( final InvocationTargetException ite ) {
                            Logger.getLogger( getClass().getName() ).log( Level.WARNING, "Exception while trying to fire event to listener: " + listener, ite );
                        }

                        // If the "hear" method returned anything, provide a
                        // Logger output
                        if( result != null ) {
                            Logger.getLogger( getClass().getName() ).log( Level.INFO, "Fired: " + event + " Listener: " + listener + " Result: " + result );
                        }

                    } else {
                        deadRefs.add( listenerRef );
                    }
                }
                typeListeners.removeAll( deadRefs );
            }
        }

    }

    /**
     * Adds the supplies <CODE>Object</CODE> as a listener for the supplied
     * type of <CODE>SmartEvent</CODE>, and no other.
     *
     * @param listener the <CODE>Object</CODE> to add as a listener
     * @param type the event type to listen to
     */
    private void addTypeSpecificListener( final Object listener, final Class<?> type ) {

        // Get or create the Set of listeners for this type
        Set<WeakReference<Object>> typeListeners = listeners.get( type );
        if( typeListeners == null ) {
            typeListeners = new HashSet<WeakReference<Object>>();
            listeners.put( type, typeListeners );
        }

        // Add the listener
        typeListeners.add( new WeakReference<Object>( listener ) );

    }
}
